#include "RNSkDomView.h"
#include "DrawingContext.h"

#include <chrono>
#include <utility>

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdocumentation"

#include "include/core/SkFont.h"

#pragma clang diagnostic pop

namespace RNSkia {
RNSkDomRenderer::RNSkDomRenderer(std::function<void()> requestRedraw, std::shared_ptr<RNSkPlatformContext> context)
    : RNSkRenderer(requestRedraw),
      _platformContext(std::move(context)),
      _renderLock(std::make_shared<std::timed_mutex>()),
      _touchCallbackLock(std::make_shared<std::timed_mutex>()),
      _renderTimingInfo("SKIA/RENDER")
{}

RNSkDomRenderer::~RNSkDomRenderer()
{
    if (_root != nullptr) {
        _root->dispose(true);
        _root = nullptr;
    }
}

bool RNSkDomRenderer::tryRender(std::shared_ptr<RNSkCanvasProvider> canvasProvider)
{
    // If we have touches we need to call the touch callback as well
    if (_currentTouches.size() > 0) {
        callOnTouch();
    }

    // We render on the main thread
    if (_renderLock->try_lock()) {
        bool result = false;
        // If we have a Dom Node we can render directly on the main thread
        if (_root != nullptr) {
            result = canvasProvider->renderToCanvas(std::bind(&RNSkDomRenderer::renderCanvas, this,
                std::placeholders::_1, canvasProvider->getScaledWidth(), canvasProvider->getScaledHeight()));
        }

        _renderLock->unlock();
        return result;
    } else {
        return false;
    }
}

void RNSkDomRenderer::renderImmediate(std::shared_ptr<RNSkCanvasProvider> canvasProvider)
{
    auto prevDebugOverlay = getShowDebugOverlays();
    setShowDebugOverlays(false);
    canvasProvider->renderToCanvas(std::bind(&RNSkDomRenderer::renderCanvas, this, std::placeholders::_1,
        canvasProvider->getScaledWidth(), canvasProvider->getScaledHeight()));
    setShowDebugOverlays(prevDebugOverlay);
}

void RNSkDomRenderer::setRoot(std::shared_ptr<JsiDomRenderNode> node)
{
    std::lock_guard<std::mutex> lock(_rootLock);
    if (_root != nullptr) {
        _root->dispose(true);
        _root = nullptr;
    }
    _root = node;
}

void RNSkDomRenderer::setOnTouchCallback(std::shared_ptr<jsi::Function> onTouchCallback)
{
    _touchCallback = onTouchCallback;
}

void RNSkDomRenderer::renderCanvas(SkCanvas *canvas, float scaledWidth, float scaledHeight)
{
    _renderTimingInfo.beginTiming();

    auto pd = _platformContext->getPixelDensity();
    canvas->clear(SK_ColorTRANSPARENT);
    canvas->save();
    canvas->scale(pd, pd);

    if (_drawingContext == nullptr) {
        _drawingContext = std::make_shared<DrawingContext>();

        _drawingContext->setRequestRedraw([weakSelf = weak_from_this()]() {
            auto self = weakSelf.lock();
            if (self) {
                self->_requestRedraw();
            }
        });
    }

    _drawingContext->setScaledWidth(scaledWidth);
    _drawingContext->setScaledHeight(scaledHeight);

    // Update canvas before drawing
    _drawingContext->setCanvas(canvas);

    try {
        // Ask the root node to render to the provided canvas
        std::lock_guard<std::mutex> lock(_rootLock);
        if (_root != nullptr) {
            _root->commitPendingChanges();
            _root->render(_drawingContext.get());
            _root->resetPendingChanges();
        }
    } catch (std::runtime_error err) {
        _platformContext->raiseError(err);
    } catch (jsi::JSError err) {
        _platformContext->raiseError(err);
    } catch (...) {
        _platformContext->raiseError(std::runtime_error("Error rendering the Skia view."));
    }

    renderDebugOverlays(canvas);

    canvas->restore();

    _renderTimingInfo.stopTiming();
}

void RNSkDomRenderer::updateTouches(std::vector<RNSkTouchInfo> &touches)
{
    std::lock_guard<std::mutex> lock(_touchMutex);
    // Add timestamp
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch())
                  .count();

    for (size_t i = 0; i < touches.size(); i++) {
        touches.at(i).timestamp = ms;
    }
    _currentTouches.push_back(std::move(touches));
}

void RNSkDomRenderer::callOnTouch()
{
    if (_touchCallback == nullptr) {
        return;
    }

    if (_touchCallbackLock->try_lock()) {
        {
            std::lock_guard<std::mutex> lock(_touchMutex);
            _touchesCache.clear();
            _touchesCache.reserve(_currentTouches.size());
            for (size_t i = 0; i < _currentTouches.size(); ++i) {
                _touchesCache.push_back(_currentTouches.at(i));
            }
            _currentTouches.clear();
        }

        // We have an onDraw method - use it to render since we don't have a
        // DOM-node yet.
        _platformContext->runOnJavascriptThread([weakSelf = weak_from_this()]() {
            auto self = weakSelf.lock();
            if (self) {
                jsi::Runtime &runtime = *self->_platformContext->getJsRuntime();
                // Set up touches
                auto size = self->_touchesCache.size();
                auto ops = jsi::Array(runtime, size);
                for (size_t i = 0; i < size; i++) {
                    auto cur = self->_touchesCache.at(i);
                    auto curSize = cur.size();
                    auto touches = jsi::Array(runtime, curSize);
                    for (size_t n = 0; n < curSize; n++) {
                        auto touchObj = jsi::Object(runtime);
                        auto t = cur.at(n);
                        touchObj.setProperty(runtime, "x", t.x);
                        touchObj.setProperty(runtime, "y", t.y);
                        touchObj.setProperty(runtime, "force", t.force);
                        touchObj.setProperty(runtime, "type", static_cast<double>(t.type));
                        touchObj.setProperty(runtime, "timestamp", static_cast<double>(t.timestamp) / 1000.0);
                        touchObj.setProperty(runtime, "id", static_cast<double>(t.id));
                        touches.setValueAtIndex(runtime, n, touchObj);
                    }
                    ops.setValueAtIndex(runtime, i, touches);
                }
                // Call on touch callback
                self->_touchCallback->call(runtime, ops, 1);
            }
            self->_touchCallbackLock->unlock();
        });
    } else {
        // We'll try next time - schedule a new redraw
        _requestRedraw();
    }
}

void RNSkDomRenderer::renderDebugOverlays(SkCanvas *canvas)
{
    if (!getShowDebugOverlays()) {
        return;
    }
    auto renderAvg = _renderTimingInfo.getAverage();
    auto fps = _renderTimingInfo.getFps();

    // Build string
    std::ostringstream stream;
    stream << "render: " << renderAvg << "ms"
           << " fps: " << fps;

    std::string debugString = stream.str();

    // Set up debug font/paints
    auto font = SkFont();
    font.setSize(14);
    auto paint = SkPaint();
    paint.setColor(SkColors::kRed);
    canvas->drawSimpleText(debugString.c_str(), debugString.size(), SkTextEncoding::kUTF8, 8, 18, font, paint);
}
} // namespace RNSkia
